<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">


  <title>TDD开发容器化的Python微服务应用(二)-blog.qikqiak.com|阳明的博客|Kubernetes|Docker|Python|Golang|Cloud Native</title>
  <meta property="og:title" content="TDD开发容器化的Python微服务应用(二)" />
  <meta name="twitter:title" content="TDD开发容器化的Python微服务应用(二)" />

  <meta name="description" content="本节课我们将我们的项目拆分成3个工程，我们也会增加一些集成测试来确保每一个服务都能够正确的运行，引入持续集成概念，最后，我们还将添加一个ReactJS的客户端。

">
  <meta property="og:description" content="本节课我们将我们的项目拆分成3个工程，我们也会增加一些集成测试来确保每一个服务都能够正确的运行，引入持续集成概念，最后，我们还将添加一个ReactJS的客户端。

">
  <meta name="twitter:description" content="本节课我们将我们的项目拆分成3个工程，我们也会增加一些集成测试来确保每一个服务都能够正确的运行，引入持续集成概念，最后，我们还将添加一个ReactJS的客户端。

">
  <meta name="author" content=""/>
  <link href='https://blog.qikqiak.com/img/favicon.ico' rel='icon' type='image/x-icon'/>
  <meta property="og:image" content="https://blog.qikqiak.com/img/avatar.jpeg" />
  <meta name="twitter:image" content="https://blog.qikqiak.com/img/avatar.jpeg" />
  <meta name="twitter:card" content="summary" />
  <meta property="og:url" content="https://blog.qikqiak.com/post/tdd-develop-python-microservice-app-part2/" />
  <meta property="og:type" content="website" />
  <meta property="og:site_name" content="阳明的博客" />

  <meta name="generator" content="Hugo 0.34" />
  <link rel="canonical" href="https://blog.qikqiak.com/post/tdd-develop-python-microservice-app-part2/" />
  <link rel="alternate" href="https://blog.qikqiak.com/index.xml" type="application/rss+xml" title="阳明的博客">
  <link href="https://cdn.bootcss.com/KaTeX/0.7.1/katex.min.css" rel="stylesheet">
  <link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/main.css?t=1235" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" />
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/pygment_highlights.css" />
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/highlight.min.css" />
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/prism.css?t=123" />
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/search.css" />
<meta name="keywords" content="">
<script>
var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://hm.baidu.com/hm.js?970de9920f6089c287a7fd366138d4e9";
  var s = document.getElementsByTagName("script")[0];
  s.parentNode.insertBefore(hm, s);
})();
</script>



<link href="https://cdn.bootcss.com/photoswipe/4.1.1/photoswipe.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/photoswipe/4.1.1/default-skin/default-skin.min.css" rel="stylesheet">



<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>

<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-69668147-3', 'auto');
ga('send', 'pageview');
</script>
<script async src='//www.google-analytics.com/analytics.js'></script>

</head>

  <body>
    <nav class="navbar navbar-default navbar-fixed-top navbar-custom">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#main-navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="https://blog.qikqiak.com/">阳明的博客</a>
    </div>

    <div class="collapse navbar-collapse" id="main-navbar">
      <ul class="nav navbar-nav navbar-right">
        
          
            <li>
              <a title="Home" href="https://ydzs.io">Home</a>
            </li>
          
        
          
            <li class="navlinks-container">
              <a class="navlinks-parent" href="javascript:void(0)">Categories</a>
              <div class="navlinks-children">
                
                  <a href="https://blog.qikqiak.com/page/archive/">Archive</a>
                
                  <a href="https://blog.qikqiak.com/tags">tags</a>
                
                  <a href="https://blog.qikqiak.com/tags/kubernetes">kubernetes</a>
                
                  <a href="https://blog.qikqiak.com/tags/python">python</a>
                
                  <a href="https://blog.qikqiak.com/tags/django">django</a>
                
                  <a href="https://blog.qikqiak.com/tags/ops">devops</a>
                
              </div>
            </li>
          
        
          
            <li class="navlinks-container">
              <a class="navlinks-parent" href="javascript:void(0)">海马学院</a>
              <div class="navlinks-children">
                
                  <a href="https://www.haimaxy.com/course/6y02wp/?utm_source=blog&amp;utm_campaign=referral&amp;utm_medium=topmenu">Python微服务实战</a>
                
                  <a href="https://www.haimaxy.com/course/pww13p/?utm_source=blog&amp;utm_campaign=referral&amp;utm_medium=topmenu">Webpack3.x 入门</a>
                
                  <a href="https://www.haimaxy.com/course/pjrqxm/?utm_source=blog&amp;utm_campaign=referral&amp;utm_medium=topmenu">Kubernetes 集群搭建</a>
                
              </div>
            </li>
          
        
          
            <li>
              <a title="About" href="https://blog.qikqiak.com/page/about/">About</a>
            </li>
          
        

        

        

        
          <li>
            <a href="#modalSearch" data-toggle="modal" data-target="#modalSearch" style="outline: none;">
              <span class="hidden-sm hidden-md hidden-lg">Search</span> <span id="searchGlyph" class="glyphicon glyphicon-search"></span>
            </a>
          </li>
          

      </ul>
    </div>

    <div class="avatar-container">
      <div class="avatar-img-border">
        
          <a title="阳明的博客" href="https://blog.qikqiak.com/">
            <img class="avatar-img" src="https://blog.qikqiak.com/img/avatar.jpeg" alt="阳明的博客" />
          </a>
        
      </div>
    </div>

  </div>
</nav>





  <div id="modalSearch" class="modal fade" role="dialog">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal">&times;</button>
          <h4 class="modal-title">Search blog.qikqiak.com</h4>
        </div>
        <div class="modal-body">
            
<div class="aa-input-container" id="aa-input-container">
    <input type="search" id="aa-search-input" class="aa-input-search" placeholder="Search for titles or URIs..." name="search" autocomplete="off" />
    <svg class="aa-input-icon" viewBox="654 -372 1664 1664">
        <path d="M1806,332c0-123.3-43.8-228.8-131.5-316.5C1586.8-72.2,1481.3-116,1358-116s-228.8,43.8-316.5,131.5  C953.8,103.2,910,208.7,910,332s43.8,228.8,131.5,316.5C1129.2,736.2,1234.7,780,1358,780s228.8-43.8,316.5-131.5  C1762.2,560.8,1806,455.3,1806,332z M2318,1164c0,34.7-12.7,64.7-38,90s-55.3,38-90,38c-36,0-66-12.7-90-38l-343-342  c-119.3,82.7-252.3,124-399,124c-95.3,0-186.5-18.5-273.5-55.5s-162-87-225-150s-113-138-150-225S654,427.3,654,332  s18.5-186.5,55.5-273.5s87-162,150-225s138-113,225-150S1262.7-372,1358-372s186.5,18.5,273.5,55.5s162,87,225,150s113,138,150,225  S2062,236.7,2062,332c0,146.7-41.3,279.7-124,399l343,343C2305.7,1098.7,2318,1128.7,2318,1164z" />
    </svg>
</div>
<script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/algoliasearch.min.js"></script>
<script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/autocomplete.min.js"></script>

<script>
var client = algoliasearch("1JDRAS0AZR", "8804ac109158bb3bb60d74ce98fa332f");
var index = client.initIndex('prod_blog');

autocomplete('#aa-search-input',
{ hint: false}, {
    source: autocomplete.sources.hits(index, {hitsPerPage: 5}),
    
    displayKey: 'name',
    
    templates: {
        
        suggestion: function(suggestion) {
            return '<span>' + '<a href="https://blog.qikqiak.com/post/' + suggestion.slug + '">' +
            suggestion._highlightResult.title.value + '</a></span>';
        }
    }
});
</script>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">close</button>
        </div>
      </div>
    </div>
  </div>

    
  
  
  




  
    <div id="header-big-imgs" data-num-img=1 data-img-src-1="https://blog.qikqiak.com/img/posts/photo-1435777940218-be0b632d06db.jpeg" data-img-desc-1="Arctic plains."></div>
  

  <header class="header-section has-img">
    
      <div class="intro-header big-img">
        
        <div class="container">
          <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
              <div class="post-heading">
                <h1>TDD开发容器化的Python微服务应用(二)</h1>
                  
                  
                    <span class="post-meta">
  Posted on January 13, 2018
  
</span>


                  
              </div>
            </div>
          </div>
        </div>
        <span class="img-desc" style="display: inline;"></span>
      </div>
    
    <div class="intro-header no-img">
      
      <div class="container">
        <div class="row">
          <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
            <div class="post-heading">
              <h1>TDD开发容器化的Python微服务应用(二)</h1>
                
                
                  <span class="post-meta">
  Posted on January 13, 2018
  
</span>


                
            </div>
          </div>
        </div>
      </div>
    </div>
  </header>


    
<div class="container" role="main">
  <div class="row">
    <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">

      <div>
          
          
          <h5 id="tags" style="margin-top: 0px;">标签:
            
                <a href="https://blog.qikqiak.com/tags/python/">python</a> &nbsp;
            
                <a href="https://blog.qikqiak.com/tags/flask/">flask</a> &nbsp;
            
                <a href="https://blog.qikqiak.com/tags/tdd/">TDD</a> &nbsp;
            
                <a href="https://blog.qikqiak.com/tags/docker/">docker</a> &nbsp;
            
                <a href="https://blog.qikqiak.com/tags/react/">react</a> &nbsp;
            
                <a href="https://blog.qikqiak.com/tags/kubernetes/">kubernetes</a> &nbsp;
            
                <a href="https://blog.qikqiak.com/tags/microservice/">Microservice</a> &nbsp;
            
          </h5>
          
      </div>

      <article role="main" class="blog-post">
        <p>本节课我们将我们的项目拆分成3个工程，我们也会增加一些集成测试来确保每一个服务都能够正确的运行，引入持续集成概念，最后，我们还将添加一个<code>ReactJS</code>的客户端。</p>

<p></p>

<blockquote>
<p>有人问我为什么这么长的文章不分拆成几篇文章啊？这样阅读起来也方便啊，然而在我自己学习的过程中，这种整个一篇文章把一件事情从头到尾讲清楚的形式是最好的，能给读者提供一种<code>沉浸式</code>的学习体验，阅读完整个文章后有种<code>酣畅淋漓</code>的感觉，所以我选择这种一篇文章的形式。</p>
</blockquote>

<p>扫描下面的二维码(或微信搜索<code>iEverything</code>)添加我微信好友(注明python)，然后可以加入到我们的<code>python</code>讨论群里面共同学习
<img src="https://blog.qikqiak.com/img/posts/wexin-qrcode.jpeg" alt="qrcode" /></p>

<h2 id="目录">目录</h2>

<ol>
<li><a href="#structure">结构</a></li>
<li><a href="#target">目标</a></li>
<li><a href="#coverage">代码覆盖</a></li>
<li><a href="#lint">代码质量</a></li>
<li><a href="#ci">持续集成</a></li>
<li><a href="#react">React 应用</a></li>
<li><a href="#test">React 测试</a></li>
<li><a href="#form">React 表单</a></li>
<li><a href="#docker">容器化 React 应用</a></li>
</ol>

<h2 id="1-结构-a-id-structure-a">1. 结构<a id="structure"></a></h2>

<ul>
<li><code>flask-microservices-main</code> - Docker Compose 文件、Nginx、管理脚本等</li>
<li><code>flask-microservices-users</code> - Flask 应用</li>
<li><code>flask-microservices-client</code> - 客户端</li>
</ul>

<h2 id="2-目标-a-id-target-a">2. 目标<a id="target"></a></h2>

<p>本节课结束后，你能学习到：</p>

<ul>
<li>用一个<code>Docker Compose</code>文件管理多个<code>git</code>仓库的服务</li>
<li>在容器中运行单元测试和集成测试</li>
<li>每个服务不依赖<code>Docker</code>运行</li>
<li>为集成测试配置<code>Travis CI</code></li>
<li>在容器中运行<code>React</code>应用</li>
<li>用<code>React</code>组件构建一个单页面应用</li>
</ul>

<h2 id="3-代码覆盖-a-id-coverage-a">3. 代码覆盖<a id="coverage"></a></h2>

<p><code>代码覆盖</code>是查找未被测试执行的代码区域的过程。不过要记住的是这并不能说明你测试代码的有效性。</p>

<p>在<code>requirements.txt</code>文件中添加依赖包：</p>

<pre><code class="language-txt">coverage==4.4.2
</code></pre>

<p>然后，我们在<code>manage.py</code>中新增一个命令：</p>

<pre><code class="language-python">import coverage
COV = coverage.coverage(
    branch=True,
    include='project/*',
    omit=[
        'project/tests/*'
    ]
)
COV.start()

@manager.command
def cov():
    &quot;&quot;&quot;Runs the unit tests with coverage.&quot;&quot;&quot;
    tests = unittest.TestLoader().discover('project/tests')
    result = unittest.TextTestRunner(verbosity=2).run(tests)
    if result.wasSuccessful():
        COV.stop()
        COV.save()
        print('Coverage Summary:')
        COV.report()
        COV.html_report()
        COV.erase()
        return 0
    return 1
</code></pre>

<p>因为新增了依赖包，所以需要重新构建容器：</p>

<pre><code class="language-shell">(tdd3)$ docker-compose -f docker-compose.yml up -d --build
</code></pre>

<p>构建完成后执行代码覆盖命令：</p>

<pre><code class="language-shell">(tdd3)$ docker-compose -f docker-compose.yml run users-service python manage.py cov
</code></pre>

<p>然后你能看到测试下面的信息：</p>

<pre><code class="language-shell">Coverage Summary:
Name                    Stmts   Miss Branch BrPart  Cover
---------------------------------------------------------
project/__init__.py        12      5      0      0    58%
project/api/models.py      13     10      0      0    23%
project/api/views.py       55      0     10      0   100%
project/config.py          16      0      0      0   100%
---------------------------------------------------------
TOTAL                      96     15     10      0    86%
</code></pre>

<p>然后我们可以看到项目根目录中多了一个<code>htmlcov</code>的文件夹，该目录下面是自动生成的代码覆盖的结果页面，我们可以在浏览器中打开<code>htmlcov/index.html</code>，可以看到一个页面：
<img src="https://blog.qikqiak.com/img/posts/code-cv-result.png" alt="code-cv-result" />
另外要记得将<code>htmlcov</code>文件夹添加到<code>.gitignore</code>文件中~</p>

<h2 id="4-代码质量-a-id-lint-a">4. 代码质量<a id="lint"></a></h2>

<p><code>Liniting</code>是一个检查你代码风格和编码错误的一个过程，对于<code>python</code>中有很多流行的<code>linting</code>工具，我们这里使用<code>Flake8</code> - 融合了<code>pep8</code>和<code>pyflakes</code>两种<code>linting</code>工具。</p>

<p>首先，在<code>requrements.txt</code>文件中添加<code>flake8</code>依赖包：</p>

<pre><code class="language-txt">flake8==3.5.0
</code></pre>

<p>同样的，重新构建容器：</p>

<pre><code class="language-shell">(tdd3)$ docker-compose -f docker-compose.yml up -d --build
</code></pre>

<p>构建成功后运行<code>flake8</code>命令检测代码：</p>

<pre><code class="language-shell">(tdd3)$ $ docker-compose -f docker-compose.yml run users-service flake8 project
Starting users-db ... done
project/__init__.py:2:1: F401 'flask.jsonify' imported but unused
project/tests/test_users.py:28:80: E501 line too long (83 &gt; 79 characters)
</code></pre>

<p>可以看到检测出两条信息，一条是<code>flask.jsonify</code>没有使用，另外一条是一行语句太长，我们先去修复一下，然后重新执行代码检测，直到没有错误信息，然后重新提交代码。</p>

<h2 id="5-持续集成-a-id-ci-a">5. 持续集成<a id="ci"></a></h2>

<p>持续集成的目的，是让产品可以快速迭代，同时还能保持高质量。它的核心措施是，代码集成到主干之前，必须通过自动化测试。只要有一个测试用例失败，就不能集成。由于我们的代码托管在<code>github</code>上面，所以选择<a href="https://travis-ci.org/">Travis CI</a>来做持续集成是一个不错的选择。</p>

<p>要触发构建工作，需要在项目根目录下面添加一个<code>.travis.yml</code>的文件：</p>

<pre><code class="language-yaml">sudo: required

services:
  - docker

env:
  DOCKER_COMPOSE_VERSION: 1.14.0

before_install:
  - sudo rm /usr/local/bin/docker-compose
  - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` &gt; docker-compose
  - chmod +x docker-compose
  - sudo mv docker-compose /usr/local/bin

before_script:
  - docker-compose -f docker-compose.yml up --build -d

script:
  - docker-compose -f docker-compose.yml run users-service python manage.py test
  - docker-compose -f docker-compose.yml run users-service flake8 project

after_script:
  - docker-compose -f docker-compose.yml down
</code></pre>

<p>上面的脚本文件看上去还是很容易的，首先安装<code>docker-compose</code>，然后在执行脚本之前构建镜像，然后执行测试、代码质量检测等操作，最后做一些清理工作。</p>

<p>然后我们登录<a href="https://travis-ci.org/">Travis CI</a>，在个人页面，确保将当前项目激活
<img src="https://blog.qikqiak.com/img/posts/travis-item.png" alt="travis-item" /></p>

<p>然后我们可以提交代码，推送到<code>GitHub</code>上，这应该会触发一次构建。然后我们同样的可以前往<code>Travis CI</code>查看构建状态：
<img src="https://blog.qikqiak.com/img/posts/travis-status.png" alt="travis-status" /></p>

<p>通过后，你最好能够添加一个<code>Travis</code>的状态徽章在<code>README.md</code>文件中，这样能让别人明白你的项目状态。</p>

<pre><code class="language-txt">[![Build Status](https://travis-ci.org/cnych/flask-microservices-users.svg?branch=master)](https://travis-ci.org/cnych/flask-microservices-users)
</code></pre>

<p>就目前来说，虽然我们的项目还相对比较简单，但是基本上我们的工作流已经能够跑得非常顺畅了：</p>

<ul>
<li>本地编码</li>
<li>提交推送到<code>GitHub</code></li>
<li>通过<code>Travis</code>来完成自动化测试</li>
</ul>

<h2 id="6-react-应用-a-id-react-a">6. React 应用<a id="react"></a></h2>

<p><a href="https://reactjs.org">React</a>是一个声明式、基于组件的<code>javascript</code>库，可以非常轻松地创建用户交互界面。为你应用的每一个状态设计简洁的视图，在数据改变时<code>React</code>也可以高效地更新渲染界面。</p>

<p>如果你还没接触过<code>React</code>的话，建议你先阅读<a href="https://reactjs.org/blog/2013/06/05/why-react.html">Why did we build React?</a>和<a href="https://reactjs.org/docs/hello-world.html">React 快速入门</a>两篇文章，当然我也建议你能学学<a href="http://babeljs.io/">Babel</a>和<a href="https://blog.qikqiak.com/post/webpack3-get-started/">Webpack</a>的相关知识，这些知识点结合在一起可以非常完美的帮助你进行<code>React</code>应用的开发。</p>

<p>首先确保你的系统中已经安装了<a href="https://nodejs.org/">Node</a>和<a href="https://www.npmjs.com/">NPM</a>：</p>

<pre><code class="language-shell">$ tdd100  node -v
v8.5.0
$ tdd100  npm -v
5.4.2
</code></pre>

<p>如果你没安装的话，根据上面两个连接你也可以非常方便的安装，这里就不详细说明了。</p>

<h3 id="6-1-初始化项目">6.1 初始化项目</h3>

<p>这里我们使用一个非常牛逼的创建<code>React</code>项目的脚手架工具：<a href="https://github.com/facebookincubator/create-react-app">Create React App</a>。
首先我们安装<code>Create React App</code>工具到全局环境中：</p>

<pre><code class="language-shell">$ npm install create-react-app --global
</code></pre>

<p>安装完成后，我们在我们的<code>flask-microservices-users</code>项目根目录下面创建一个新的文件夹：<code>client</code>，然后初始化我们的项目结构：</p>

<pre><code class="language-shell">$ mkdir client &amp;&amp; cd client
$ create-react-app .
</code></pre>

<p>项目结构创建完成后，相关的依赖包也已经安装好，完成后，我们可以启动服务：</p>

<pre><code class="language-shell">$ npm start
</code></pre>

<p>服务启动完成后，<code>Create React App</code>工具会自动用默认的浏览器打开：<a href="http://localhost:3000">http://localhost:3000</a>。
<img src="https://blog.qikqiak.com/img/posts/react-index.png" alt="react-index" /></p>

<p>确保上面这些都正常后，关掉服务(Crtl + C)，为了简化我们的开发过程，我们可以通知<code>npm</code>不要为项目创建<code>package-lock.json</code>文件：</p>

<pre><code class="language-shell">$ echo 'package-lock=false' &gt;&gt; .npmrc
</code></pre>

<ul>
<li>可以通过文档<a href="https://docs.npmjs.com/files/npmrc">npm docs</a>查看关于配置文件<code>.npmrc</code>的更多信息。</li>
<li>对<code>package-lock.json</code>文件不太清楚的，可以查看这篇文章:<a href="https://jpospisil.com/2017/06/02/understanding-lock-files-in-npm-5.html">Understanding lock files in NPM 5</a>进行了解。</li>
</ul>

<p>现在来开始创建创我们的第一个组件吧~~~</p>

<h3 id="6-2-第一个组件">6.2 第一个组件</h3>

<p>为了让我们的项目结构看起来更加简单，我们移除<strong>src</strong>目录下面的*App.css*，*App.js*，*App.test.js*以及*index.css*文件，然后更新*index.js*文件：</p>

<pre><code class="language-javascript">import React from 'react';
import ReactDOM from 'react-dom';

const App = () =&gt; {
    return (
      &lt;div className=&quot;container&quot;&gt;
        &lt;div className=&quot;row&quot;&gt;
          &lt;div className=&quot;col-md-4&quot;&gt;
            &lt;br /&gt;
            &lt;h1&gt;All Users&lt;/h1&gt;
            &lt;hr /&gt;&lt;br /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    )
};

ReactDOM.render(
  &lt;App /&gt;,
  document.getElementById('root')
);
</code></pre>

<p>上面代码做了几件事：</p>

<ul>
<li>导入<code>React</code>和<code>ReactDom</code>类后，我们创建了一个叫<code>App</code>的函数，这是<code>ES6</code>里面的箭头函数的写法，然后该函数返回的是一个<a href="https://facebook.github.io/jsx/">JSX</a>格式的对象。</li>
<li>我们用<code>ReactDOM</code>的<code>render</code>方法将我们的<code>App</code>组件挂载到了一个ID为<code>root</code>的*HTML*元素上。</li>
</ul>

<blockquote>
<p>注意<code>public</code>目录下面的<code>index.html</code>文件的<code>&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;</code>，这就是被挂载的地方。另外对于还不太了解<code>ES6</code>的同学，不用担心，实际上是可以直接跳过<code>javascript</code>的语法，直接学习<code>ES6</code>的(虽然不太推荐这样)，<code>ES6</code>比原生的<code>javascript</code>更加系统、更加容易学习、也更加接近你所学习过的其他编程语言(<a href="http://babeljs.io/learn-es2015/">点击这里前去学习吧</a>)</p>
</blockquote>

<p>然后在<code>public</code>目录下的<strong>index.html</strong>文件的<strong>head</strong>区域添加<code>bootstrap</code>的<strong>css</strong>样式文件：</p>

<pre><code class="language-html">&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css&quot;&gt;
</code></pre>

<h3 id="6-3-基于类的组件">6.3 基于类的组件</h3>

<p>现在我们来将我们的<code>App</code>组件更改成基于类的形式：</p>

<pre><code class="language-javascript">import React, {Component} from 'react';
import ReactDOM from 'react-dom';

class App extends Component {
    constructor() {
        super();
    }
    render() {
        return (
          &lt;div className=&quot;container&quot;&gt;
            &lt;div className=&quot;row&quot;&gt;
              &lt;div className=&quot;col-md-4&quot;&gt;
                &lt;br /&gt;
                &lt;h1&gt;All Users&lt;/h1&gt;
                &lt;hr /&gt;&lt;br /&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        )
    }
}

ReactDOM.render(
  &lt;App /&gt;,
  document.getElementById('root')
);
</code></pre>

<p>然后我们执行<code>npm start</code>可以发现其实上面的基于类的组件和前面的函数组件，最后的输出都是一模一样的，我们在后面会慢慢发现二者之间的区别的。</p>

<h3 id="6-4-ajax请求">6.4 AJAX请求</h3>

<p>为了连接客户端和服务端数据，我们在<code>App</code>类中增加一个<code>getUsers()</code>的方法，我们用一个非常流行的库：<a href="https://github.com/axios/axios">Axios</a>来进行网络请求：
首先先安装<code>axios</code>包：</p>

<pre><code class="language-shell">$ npm install axios --save
</code></pre>

<p>然后在<code>App</code>类中添加<code>getUsers</code>函数：</p>

<pre><code class="language-javascript">import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';

class App extends Component {
    constructor() {
        super();
    }
    getUsers() {
      axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`).then(res =&gt; {
        console.log(res);
      }).catch(err =&gt; {
        console.log(err);
      });
    }
    render() {
        return (
          &lt;div className=&quot;container&quot;&gt;
            &lt;div className=&quot;row&quot;&gt;
              &lt;div className=&quot;col-md-4&quot;&gt;
                &lt;br /&gt;
                &lt;h1&gt;All Users&lt;/h1&gt;
                &lt;hr /&gt;&lt;br /&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        )
    }
}

ReactDOM.render(
  &lt;App /&gt;,
  document.getElementById('root')
);
</code></pre>

<p>当然不要忘记在顶部导入<code>axios</code>包哦~</p>

<p>为了验证上面的功能，我们要先开启我们的服务端，打开一个新的终端窗口，然后定位到<code>flask-microservices-users</code>项目根目录，还记得前面的启动<code>docker-compose</code>的命令吗？</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml up -d
</code></pre>

<p>为了确保我们服务端的代码是正常工作的，我们还需要执行下我们的测试命令：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml run users-service python manage.py test
</code></pre>

<p>测试通过后，我们回到<code>React</code>项目，从上面的代码中可以看出我们需要增加一个<code>process.env.REACT_APP_USERS_SERVICE_URL</code>的环境变量，首先杀掉<code>React</code>的服务(Ctrl + C)，然后执行下面的命令：</p>

<pre><code class="language-shell">$ export REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001
</code></pre>

<blockquote>
<p>注意所有的自定义的环境变量必须要已<code>REACT_APP_</code>开头，更多信息可以查看<a href="https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-custom-environment-variables">官方文档</a></p>
</blockquote>

<p>现在为了验证<code>getUsers()</code>方法，我们可以在构造函数<code>constructor()</code>中先调用：</p>

<pre><code class="language-javascript">constructor() {
  super();
  this.getUsers();
}
</code></pre>

<p>这样当我们的<code>App</code>组件被实例化的时候就会调用<code>getUsers()</code>方法了。我们来运行命令：<code>npm start</code>打开我们的<code>React</code>应用，然后打开<a href="https://developer.chrome.com/devtools">Chrome 开发者工具</a>(强烈推荐把<strong>chrome</strong>浏览器设置为默认浏览器，这货对于前端开发者来说真的是神器，在页面右键选择审查元素即可打开)，然后打开<code>JavaScript Console</code>控制台，你将能看到下面的错误信息：
<img src="https://blog.qikqiak.com/img/posts/react-error1.png" alt="react error" />
简单来说，就是我们正在发起一个<a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">跨域</a>的<strong>AJAX</strong>请求(从<code>http://localhost:3000</code>到<code>http://127.0.0.1:5001</code>)，这是违反浏览器的<strong>同源策略</strong>的，所以该请求被拒绝了。幸运的时候我们可以通过<a href="https://flask-cors.readthedocs.io">Flask-CORS</a>扩展在服务端来处理跨域的请求。</p>

<p>回到<strong>flaks-users-service</strong>的目录，增加<strong>Flask-CORS</strong>包在<code>requirements.txt</code>文件中：</p>

<pre><code class="language-txt">flask-cors==3.0.3
</code></pre>

<p>为方便我们开发调试，我们设置所有的请求都运行跨域操作(切记，生产环境绝对不能这样做)，更新<code>flaks-users-service/project/__init__.py</code>文件的<code>create_app()</code>方法：</p>

<pre><code class="language-python">from flask_cors import CORS
def create_app():
    # 初始化应用
    app = Flask(__name__)

    # 运行跨域
    CORS(app)

    # 环境配置
    app_settings = os.getenv('APP_SETTINGS')
    app.config.from_object(app_settings)
    # 安装扩展
    db.init_app(app)
    # 注册blueprint
    from project.api.views import users_blueprint
    app.register_blueprint(users_blueprint)
    return app
</code></pre>

<p>不要忘记在文件的顶部导入<code>flask_cors</code>包：</p>

<pre><code class="language-python">from flask_cors import CORS
</code></pre>

<p>由于我们新增了依赖包，所以我们需要重新构建我们的镜像：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml up -d --build
</code></pre>

<p>然后更新、初始化数据库：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml run users-service python manage.py recreate_db
$ docker-compose -f docker-compose.yml run users-service python manage.py seed_db
</code></pre>

<p>然后将我们的<code>React</code>应用服务打开(<code>npm start</code>)，一样的操作在浏览器中打开<code>JavaScript Console</code>控制终端，这下我们应该可以看到正常的网络请求的打印结果：<code>console.log(res)</code>。
<img src="https://blog.qikqiak.com/img/posts/react-console1.png" alt="react console1" /></p>

<p>我们前面在写获取用户列表的<code>API</code>的时候，返回的数据结构是这样的：</p>

<pre><code class="language-json">{
  'status': 'success',
  'data': {
      'users': users_list
  }
}
</code></pre>

<p>还记得吗(<code>project/api/views.py</code>文件中的<code>get_users</code>方法)？我们来更改<code>axios</code>获取成功后的打印语句，可以很方便的拿到用户列表数据：</p>

<pre><code class="language-javascript">getUsers() {
  axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`)
  .then((res) =&gt; { console.log(res.data.data.users); })
  .catch((err) =&gt; { console.log(err); })
}
</code></pre>

<p>（想想为什么是<code>res.data.data.users</code>？）现在你可以在<strong>Javascript Console</strong>终端中看到两个用户对象的数组打印出来了：</p>

<pre><code class="language-json">[
  {&quot;created_at&quot;:&quot;Sat, 13 Jan 2018 06:12:40 GMT&quot;,&quot;email&quot;:&quot;qikqiak@gmail.com&quot;,&quot;id&quot;:1,&quot;username&quot;:&quot;cnych&quot;},
  {&quot;created_at&quot;:&quot;Sat, 13 Jan 2018 06:12:40 GMT&quot;,&quot;email&quot;:&quot;icnych@gmail.com&quot;,&quot;id&quot;:2,&quot;username&quot;:&quot;chyang&quot;}
]
</code></pre>

<p>现在有个问题是，我们是在构造函数中调用的<code>getUsers()</code>方法，而构造函数<code>constructor()</code>是在组件被挂载到<code>DOM</code>之前调用的，如果<code>AJAX</code>请求在组件被挂载完成之前比预期花费了更多的时间会出现什么情况呢？这有可能会造成<strong>竞争危害</strong>，什么意思？就是页面上的数据可能现在能够渲染出来，另外一次又可能渲染不出来(因为这个时候数据在组件挂载完成后还没完成请求，明白了吗？)，这取决与我们的<code>AJAX</code>请求是否能够在组件挂载完成之前完成请求，对吧。不过不用担心，<code>React</code>定义了一系列的生命周期函数，可以很方便的来解决这个问题。</p>

<h3 id="6-5-组件声明周期">6.5 组件声明周期</h3>

<p>基于类的组件有一个特定的函数，它们在组件的生命周期各个阶段执行。这些函数被称作<strong>生命周期</strong>方法，我们可以先花点时间看看<a href="https://reactjs.org/docs/react-component.html#the-component-lifecycle">官方文档</a>来简单的学习下每个声明周期方法，看下这些方法都是在什么地方被调用的。
简单总结下：方法中带有前缀<strong>will</strong>的在特定环节之前被调用，而带有前缀<strong>did</strong>的方法则会在特定环节之后被调用。</p>

<h4 id="挂载">挂载</h4>

<p>下面这些方法会在组件实例被创建和插入<code>DOM</code>中时被调用：</p>

<ul>
<li>constructor()</li>
<li>componentWillMount()：会在组件<code>render</code>之前执行，并且永远都只执行一次。</li>
<li>render()</li>
<li>componentDidMount()：会在组件加载完毕之后立即执行。这个时候组件已经生成了对应的<code>DOM</code>结构</li>
</ul>

<h4 id="更新">更新</h4>

<p>属性或者状态的改变会触发一次更新。当一个组件在被重新渲染时，下面这些方法会被调用：</p>

<ul>
<li>componentWillReceiveProps()：在组件接收到一个新的<code>prop</code>时被执行</li>
<li>shouldComponentUpdate()</li>
<li>componentWillUpdate()</li>
<li>render()</li>
<li>componentDidUpdate()</li>
</ul>

<h4 id="卸载">卸载</h4>

<p>当一个组件从<code>DOM</code>中移除时，会调用下面的方法：</p>

<ul>
<li>componentWillUnmount()</li>
</ul>

<p>说了这么多，我们应该在哪个方法里面来做我们的网络请求呢？实际上<code>ES6</code>中的构造函数和<code>componentWillMount</code>函数是一致的，上面我们已经知道构造函数中执行网络请求甚至是所有的异步操作都不是好的选择，在<code>componentDidMount</code>函数中执行异步操作是最好的时机，可以通过<a href="https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/">Where to Fetch Data: componentWillMount vs componentDidMount</a>了解到原因。</p>

<p>更改<code>client/src/index.js</code>代码，在<code>App</code>类中增加方法：</p>

<pre><code class="language-javascript">class App extends Component {
  constructor() {
    super();
  };
  componentDidMount() {
    this.getUsers();
  };
  getUsers() {
    axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`)
    .then((res) =&gt; { console.log(res.data.data.users); })
    .catch((err) =&gt; { console.log(err); })
  };
  render() {
    return (
      &lt;div className=&quot;container&quot;&gt;
        &lt;div className=&quot;row&quot;&gt;
          &lt;div className=&quot;col-md-4&quot;&gt;
            &lt;br/&gt;
            &lt;h1&gt;All Users&lt;/h1&gt;
            &lt;hr/&gt;&lt;br/&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    )
  }
};

ReactDOM.render(
  &lt;App /&gt;,
  document.getElementById('root')
);
</code></pre>

<p>确保上面的<code>React</code>应用仍然能够正常工作</p>

<h3 id="6-6-state-状态">6.6 State（状态）</h3>

<p><code>state</code>是<code>React</code>中非常重要的一个概念，一个组件的显示形态可以由它的数据状态和配置参数来决定，一个组件可以拥有自己的状态，状态的改变可以让<code>React</code>高效的更新界面。
现在我们为组件<code>App</code>增加一个<strong>users</strong>的状态数据，然后我们可以使用<code>setState()</code>方法来更新状态数据，更新<code>getUsers()</code>方法：</p>

<pre><code class="language-javascript">getUsers() {
  axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`).then(res =&gt; {
    this.setState({
        users: res.data.data.users
    });
  }).catch(err =&gt; {
    console.log(err);
  });
}
</code></pre>

<p>然后我们在构造函数中增加状态<code>users</code>：</p>

<pre><code class="language-javascript">constructor() {
    super();
    this.state = {
        users: []
    }
}
</code></pre>

<p>我们可以看到，默认初始化的时候<code>users</code>数据是一个空数组，在<code>getUsers()</code>方法调用成功后，我们调用<code>setState()</code>方法更新了<code>users</code>状态。</p>

<blockquote>
<p>查看<a href="https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly">官方文档</a>学习正确的使用<code>state</code>。</p>
</blockquote>

<p>然后我们就可以更新<code>rendor()</code>方法来将状态数据渲染到页面中：</p>

<pre><code class="language-javascript">render() {
    return (
      &lt;div className=&quot;container&quot;&gt;
        &lt;div className=&quot;row&quot;&gt;
          &lt;div className=&quot;col-md-4&quot;&gt;
            &lt;br /&gt;
            &lt;h1&gt;All Users&lt;/h1&gt;
            &lt;hr /&gt;&lt;br /&gt;
            {
                this.state.users.map(user =&gt; {
                    return (
                        &lt;h4
                          key={ user.id }
                          className=&quot;well&quot;
                        &gt;{ user.username }
                        &lt;/h4&gt;
                    )
                })
            }
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    )
}
</code></pre>

<p>上面的<code>render()</code>函数：</p>

<ul>
<li>我们循环(用的<strong>ES6的map</strong>方法)<code>users</code>状态数据，每次循环中创建了一个新的<strong>H4</strong>元素，这也是为什么我们初始化的<code>users</code>是一个空数组的原因，避免初始化的时候出错。</li>
<li>注意<strong>H4</strong>元素中我们增加了一个属性<code>key</code>：<code>React</code>通过该值来跟踪每一个元素，每个<code>key</code>对应一个组件，相同的<code>key</code><strong>React</strong>会认为是同一个组件，这样后续相同的<code>key</code>对应组件都不会被创建，简单的来说就是该属性是用来保证<code>React</code>高效的一个重要标识，在循环中一定要加上该属性。<a href="https://facebook.github.io/react/docs/lists-and-keys.html#keys">查看官方文档</a>了解更多关于<code>key</code>的概念。</li>
</ul>

<p>到这里，其实我们已经可以看到<code>React</code>应用的效果了，如下图：
<img src="https://blog.qikqiak.com/img/posts/reactapp-index1.png" alt="react-app-index" /></p>

<h3 id="6-7-组件">6.7 组件</h3>

<p>我们知道<code>React</code>是一个组件化的库，我们这里可以将用户列表做成一个组件，这样其他任何地方要使用用户列表的话，只需要将这个组件引入就行了，是不是很方便~~~</p>

<p>首先在<strong>src</strong>目录下面新建<strong>components</strong>目录，在该目录下新建文件：*UserList.jsx*：</p>

<pre><code class="language-javascript">import React from 'react';

const UserList = (props) =&gt; {
    return (
        &lt;div&gt;
          {
            props.users.map(user =&gt; {
              return (
                &lt;h4
                  key={ user.id }
                  className=&quot;well&quot;
                &gt;{ user.username }
                &lt;/h4&gt;
              )
            })
          }
        &lt;/div&gt;
    )
};

export default UserList;
</code></pre>

<p>注意这里，为什么我们使用一个函数组件而不是基于类的组件呢？注意在该组件中我们使用<code>props</code>代替了<code>state</code>：</p>

<ul>
<li><code>Props</code>：数据通过<code>props</code>向下传递，是只读的</li>
<li><code>State</code>：数据绑定到一个组件上，是可读写的</li>
</ul>

<blockquote>
<p>可以查看文章<a href="http://lucybain.com/blog/2016/react-state-vs-pros/">ReactJS: Props vs. State</a>了解更多属性和状态的区别。</p>
</blockquote>

<p>限制基于类的（有状态）组件数量是一个好的习惯，因为它们可以操作状态，因为不太可预测，不太可控制。如果你只需要渲染数据（就像我们的<strong>UserList</strong>组件）,使用一个函数类的组件是一个更好的选择。</p>

<p>现在我们需要在父组件中通过状态数据传递给子组件，首先在<code>index.js</code>文件中先引入我们的用户列表组件：</p>

<pre><code class="language-javascript">import UserList from './components/UserList';
</code></pre>

<p>然后更新<code>render()</code>方法：</p>

<pre><code class="language-javascript">render() {
    return (
      &lt;div className=&quot;container&quot;&gt;
        &lt;div className=&quot;row&quot;&gt;
          &lt;div className=&quot;col-md-4&quot;&gt;
            &lt;br /&gt;
            &lt;h1&gt;All Users&lt;/h1&gt;
            &lt;hr /&gt;&lt;br /&gt;
            &lt;UserList users={this.state.users} /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    )
}
</code></pre>

<p>注意看上面是怎样引入用户列表组件的：<strong><UserList users={this.state.users} /></strong>，这样<code>App</code>组件将状态数据<code>users</code>通过属性的形式传递给了子组件<code>UserList</code>，然后子组件中通过属性<code>users</code>进行数据渲染，整个流程明白了吗？</p>

<p>到这里请确保<code>React</code>应用能够正常的工作，在浏览器中打开<code>http://localhost:3000</code>能得到上面相同的结果，然后我们对我们的代码进行一些<code>Review</code>，然后提交到<code>github</code>上去。</p>

<h2 id="7-react-测试-a-id-test-a">7. React 测试<a id="test"></a></h2>

<p>在了解上面的一些<code>React</code>基本概念过后，我们就可以来使用<code>TDD</code>的思想来进行我们开发了。</p>

<p><code>Create React App</code>默认使用<a href="https://facebook.github.io/jest/">Jest</a>进行测试，所以你需要先安装<a href="https://www.npmjs.com/package/jest-cli">jest-cli</a>，进入到<code>client</code>根目录：</p>

<pre><code class="language-shell">$ npm install jest-cli@20.0.4 --save-dev
</code></pre>

<blockquote>
<p>在安装的时候最后和我这里的版本保持一致，因为其他的版本可能会有其他问题，就需要自己去踩坑了哦~~~</p>
</blockquote>

<p>另外我们还需要使用<a href="https://github.com/airbnb/enzyme">Enzyme</a> - Airbnb出品的一个专门用于测试<code>React</code>组件的非常好用的库，同样的安装<code>Enzyme</code>：</p>

<pre><code class="language-shell">$ npm install --save-dev enzyme@3.1.0 enzyme-adapter-react-16@1.0.2
</code></pre>

<p>为了让<code>Enzyme</code>配置<code>React 16</code>的adapter，在<strong>src</strong>目录下面新增一个<code>setupTests.js</code>的文件：</p>

<pre><code class="language-javascript">import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({adapter: new Adapter()});
</code></pre>

<blockquote>
<p>关于<code>Enzyme</code>的更多用法，可以前往<a href="http://airbnb.io/enzyme/#installation">官方文档</a>查看。</p>
</blockquote>

<p>配置完成后，就可以运行测试命令了：</p>

<pre><code class="language-shell">$ npm test
No tests found related to files changed since last commit.
</code></pre>

<p>默认上面的测试命令运行在<code>watch</code>模式下面，所以你每次更改了代码后都会重新执行一遍。</p>

<p><sup>112</sup>&frasl;<sub>5000</sub>
与Jest一起，我们将使用Enzyme，一个专门用于测试React组件的梦幻般的实用程序库。</p>

<h3 id="组件测试">组件测试</h3>

<p>在<code>components</code>文件夹下面新建文件<strong>UserList.test.js</strong>：</p>

<pre><code class="language-javascript">import React from 'react';
import { shallow } from 'enzyme';
import UserList from './UserList';

const users = [
    {
        'active': true,
        'email': 'icnych@gmail.com',
        'id': 1,
        'username': 'cnych'
    }, {
        'active': true,
        'email': 'qikqiak@gmail.com',
        'id': 2,
        'username': 'qikqiak'
    }
];

test('UserList renders property', () =&gt; {
    const wrapper = shallow(&lt;UserList users={users} /&gt;);
    const element = wrapper.find('h4');
    expect(element.length).toBe(2);
    expect(element.get(0).props.className).toBe('well');
    expect(element.get(0).props.children).toBe('cnych');
});
</code></pre>

<p>上面的测试代码中，我们使用了一个<strong>shallow</strong>的方法来创建<strong>UserList</strong>组件，然后我们就可以对它的输出进行断言测试了，使用<strong>shallow</strong>可以完全的隔离组件进行测试，这有助于子组件不会对测试结果产生影响。</p>

<blockquote>
<p>有关<code>shallow</code>的更多信息，以及组件渲染的其他方法，可以参阅<a href="https://stackoverflow.com/questions/38710309/when-should-you-use-render-and-shallow-in-enzyme-react-tests/38747914#38747914">StackOverflow 这篇文章</a>，当然你也可以到官网查看。</p>
</blockquote>

<p>然后继续运行测试，看是否正常通过：</p>

<pre><code class="language-shell">$ npm run test
 PASS  src/components/UserList.test.js
  ✓ UserList renders property (11ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.38s
Ran all test suites related to changed files.
</code></pre>

<p>如果测试没通过，请检查下测试代码，已经测试依赖包的版本是否兼容。</p>

<h3 id="快照测试">快照测试</h3>

<p>上面的测试通过后，我们可以增加一个快照测试来保证<code>UI</code>界面不会改变。增加下面的代码到<code>UserList.test.js</code>文件中：</p>

<pre><code class="language-javascript">test('UserList renders a snapshot properly', () =&gt; {
  const tree = renderer.create(&lt;UsersList users={users}/&gt;).toJSON();
  expect(tree).toMatchSnapshot();
});
</code></pre>

<p>记得在文件顶部导入<code>renderer</code>：</p>

<pre><code class="language-javascript">import renderer from 'react-test-renderer';
</code></pre>

<p>然后继续运行测试命令，在第一次测试运行时，会将组件输出的快照保存到<code>__snapshots__</code>文件夹中。 然后在以后的测试中，将新输出的快照与保存的快照进行比较，如果不同，则测试失败。</p>

<p>由于测试在<code>watch</code>模式下，我们将<strong>UserList</strong>组件中的<strong>{user.username}</strong>更改为<strong>{user.email}</strong>，保存后触发新的测试，我们可以看到测试失败了：</p>

<pre><code class="language-shell">Snapshot Summary
 › 1 snapshot test failed in 1 test suite. Inspect your code changes or press `u` to update them.

Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   1 failed, 1 total
Time:        1.609s
Ran all test suites related to changed files.
</code></pre>

<p>根据上面的提示，如果我们的目的就是要更改上面的组件，那么就需要更新快照，我们只需要输入<code>u</code>键即可：</p>

<pre><code class="language-shell">Watch Usage
 › Press a to run all tests.
 › Press u to update failing snapshots.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
</code></pre>

<p>输入<code>u</code>键过后我们可以看到快照测试通过了，当然了我们还需要相应的更改上面组件测试中的期望值，确保测试通过后，将文件夹<code>__snapshots__</code>加入到<strong>.gitignore</strong>文件中，然后提交代码。</p>

<h2 id="8-react-表单-a-id-form-a">8. React 表单<a id="form"></a></h2>

<p>这一节，我们将创建一个添加用户的功能组件。在<code>client/src/components</code>目录下面添加两个新文件：</p>

<ul>
<li>AddUser.jsx</li>
<li>AddUser.test.js</li>
</ul>

<p>添加测试代码：</p>

<pre><code class="language-javascript">import React from 'react';
import { shallow } from 'enzyme';
import renderer from 'react-test-renderer';

import AddUser from './AddUser';

test('AddUser renders properly', () =&gt; {
  const wrapper = shallow(&lt;AddUser /&gt;);
  const element = wrapper.find('form');
  expect(element.find('input').length).toBe(3);
  expect(element.find('input').get(0).props.name).toBe('username');
  expect(element.find('input').get(1).props.name).toBe('email');
  expect(element.find('input').get(2).props.name).toBe('submit');
});
</code></pre>

<p>现在运行测试代码肯定会报错的，然后根据上面我们的测试代码，来编写<code>AddUser</code>组件代码：</p>

<pre><code class="language-javascript">import React from 'react';

const AddUser = (props) =&gt; {
  return (
    &lt;form&gt;
      &lt;div className=&quot;form-group&quot;&gt;
        &lt;input
          type=&quot;text&quot; name=&quot;username&quot;
          className=&quot;form-control input-lg&quot;
          placeholder=&quot;Enter a username&quot;
          required
        /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;form-group&quot;&gt;
        &lt;input
          name=&quot;email&quot;
          className=&quot;form-control input-lg&quot;
          type=&quot;email&quot;
          placeholder=&quot;Enter an email address&quot;
          required
        /&gt;
      &lt;/div&gt;
      &lt;input
        type=&quot;submit&quot;
        className=&quot;btn btn-primary btn-lg btn-block&quot;
        value=&quot;Submit&quot;
      /&gt;
    &lt;/form&gt;
  );
};

export default AddUser;
</code></pre>

<p>然后我们在<code>index.js</code>文件中引入<code>AddUser</code>组件：</p>

<pre><code class="language-shell">import AddUser from './components/AddUser';
</code></pre>

<p>然后在<strong>render()</strong>方法中增加组件：</p>

<pre><code class="language-javascript">render() {
  return (
    &lt;div className=&quot;container&quot;&gt;
      &lt;div className=&quot;row&quot;&gt;
        &lt;div className=&quot;col-md-6&quot;&gt;
          &lt;br/&gt;
          &lt;h1&gt;All Users&lt;/h1&gt;
          &lt;hr/&gt;&lt;br/&gt;
          &lt;AddUser/&gt;
          &lt;br/&gt;
          &lt;UsersList users={this.state.users}/&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
};
</code></pre>

<p>确保我们之前的后端服务是启动的状态：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml up -d --build
</code></pre>

<p>然后定位到 client 目录下面，先导入我们的环境变量：</p>

<pre><code class="language-shell">$ export REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001
$ npm run start
</code></pre>

<p>然后在自动打开的浏览器中<code>http://localhost:3000/</code>应该能看到下面的界面：
<img src="https://blog.qikqiak.com/img/posts/WX20180124-172026.png" alt="index" />
然后在<code>AddUser.test.js</code>文件中新增一个测试：</p>

<pre><code class="language-javascript">test('AddUser renders a snapshot properly', () =&gt; {
    const tree = renderer.create(&lt;AddUser/&gt;).toJSON();
    expect(tree).toMatchSnapshot();
});
</code></pre>

<p>然后我们执行测试，确保测试能够正常通过</p>

<pre><code class="language-shell">$ npm run test
</code></pre>

<p>由于这是一个单页面应用程序，接下来我们希望在提交表单的时候阻止浏览器刷新页面，这样的用户体验会好很多的。要完成该功能需要以下4步：</p>

<ul>
<li>处理表单提交事件</li>
<li>获取用户输入</li>
<li>发送<code>AJAX</code>请求</li>
<li>更新页面</li>
</ul>

<h3 id="处理表单提交事件">处理表单提交事件</h3>

<p>为了让我们自己能够处理<code>submit</code>提交事件，只需要在<code>AddUser.jsx</code>文件中更新下<code>form</code>表单元素即可：</p>

<pre><code class="language-html">&lt;form onSubmit={(event) =&gt; event.preventDefault()}&gt;
</code></pre>

<p>大家记住<code>event.preventDefault()</code>这个方法，是用来阻止元素的默认处理事件的，以后肯定会用到的，然后我们随意输入一个用户名或邮件地址，然后尝试提交一次表单，我们可以看到页面没有任何反应，这正是我们所希望的，因为我们阻止了正常的事件行为。</p>

<p>然后我们在<code>client/src/index.js</code>文件中，为<code>App</code>组件添加一个新的方法：</p>

<pre><code class="language-javascript">addUser(event) {
  event.preventDefault();
  console.log('sanity check!');
};
</code></pre>

<p>由于 AddUser 是一个函数组件，所以我们需要通过<code>props</code>来传递上面的添加方法，更新 render 方法下面的 AddUser 元素，如下所示：</p>

<pre><code class="language-javascript">&lt;AddUser addUser={this.addUser} /&gt;
</code></pre>

<p>然后我们需要更新 App 组件的构造函数：</p>

<pre><code class="language-javascript">constructor() {
    super();
    this.state = {
        users: []
    };
    this.addUser = this.addUser.bind(this);
}
</code></pre>

<p>注意上面的<strong>bind</strong>方法，我们通过 bind 方法来手动绑定 this 的上下文。如果没有绑定的话，方法内的上下文是不正确的。</p>

<blockquote>
<p>关事件处理的更多信息，可以查看 React 关于事件处理的<a href="https://reactjs.org/docs/handling-events.html">官方文档</a>。</p>
</blockquote>

<p>没有它，这个方法内的上下文将不会有正确的上下文。 想要测试这个？ 只需将console.log（this）添加到addUser（），然后提交表单即可。 什么是上下文？ 删除绑定并再次测试。 现在的情况是什么？</p>

<p>然后在更新<code>AddUser</code>组件中的 form 元素：</p>

<pre><code class="language-javascript">&lt;form onSubmit={(event) =&gt; props.addUser(event)}&gt;
</code></pre>

<p>然后切换到浏览器中，随便输入用户名和邮件，点击提交按钮，在<code>JavaScript Console</code>终端（还记得怎么查看吗？）中可以看到<code>sanity check!</code>打印出来。</p>

<h3 id="获取用户输入">获取用户输入</h3>

<p>我们将使用<a href="https://reactjs.org/docs/forms.html#controlled-components">受控组件</a>来获取用户提交的数据。先增加两个新的属性在 App 组件的 state 对象上：</p>

<pre><code class="language-javascript">this.state = {
  users: [],
  username: '',
  email: ''
};
</code></pre>

<p>然后通过属性传递给 AddUser 组件：</p>

<pre><code class="language-javascript">&lt;AddUser
  username={this.state.username}
  email={this.state.email}
  addUser={this.addUser}
/&gt;
</code></pre>

<p>现在我们在 AddUser 组件中就可以通过<code>props</code>来访问 username 和 email 了，更新 AddUser 组件：</p>

<pre><code class="language-javascript">import React from 'react';

const AddUser = (props) =&gt; {
    return (
      &lt;form onSubmit={(event) =&gt; props.addUser(event)}&gt;
          &lt;div className=&quot;form-group&quot;&gt;
            &lt;input
              type=&quot;text&quot; name=&quot;username&quot;
              className=&quot;form-control input-lg&quot;
              placeholder=&quot;Enter a username&quot;
              required
              value={props.username}
            /&gt;
          &lt;/div&gt;
          &lt;div className=&quot;form-group&quot;&gt;
            &lt;input
              name=&quot;email&quot;
              className=&quot;form-control input-lg&quot;
              type=&quot;email&quot;
              placeholder=&quot;Enter an email address&quot;
              required
              value={props.email}
            /&gt;
          &lt;/div&gt;
          &lt;input
            type=&quot;submit&quot;
            className=&quot;btn btn-primary btn-lg btn-block&quot;
            value=&quot;Submit&quot;
          /&gt;
        &lt;/form&gt;
    );
};

export default AddUser;
</code></pre>

<p>然后我们可以到浏览器中测试下，随意输入一些值，你会发现什么都不能输入，这是为什么呢？仔细看上面的 input 组件的 value 值，是通过<code>props</code>来获取的，而传递给该组件的值是通过父组件的 state 传递过来的，因为父组件的 state 对象中的 username 和 email 一直都是<strong>空字符串</strong>，所以我们这里的输入值没有任何效果。</p>

<blockquote>
<p>将父组件中的 state 对象中的 username 默认值更改成其他字符串，再看看效果呢?是不是和我们的分析是一致的。</p>
</blockquote>

<p>那么我们怎么来更新父组件中的状态值呢，这样当我们在输入框中输入文本的时候就可以看到变化了。</p>

<p>首先，在 App 组件中新增一个<code>handleChange</code>的方法：</p>

<pre><code class="language-javascript">handleChange(event){
  const obj = {};
  obj[event.target.name] = event.target.value;
  this.setState(obj);
}
</code></pre>

<p>然后同样的，在构造函数中添加一个 bind：</p>

<pre><code class="language-javascript">this.handleChange = this.handleChange.bind(this);
</code></pre>

<p>然后通过<code>props</code>将该方法传递给<code>AddUser</code>组件：</p>

<pre><code class="language-javascript">&lt;AddUser
  username={this.state.username}
  email={this.state.email}
  handleChange={this.handleChange}
  addUser={this.addUser}
/&gt;
</code></pre>

<p>然后，我们就需要更新 AddUser 组件了，我们监听 input 元素的 <code>onChange</code> 方法，当 input 元素发送改变的时候，就会触发该调用：</p>

<pre><code class="language-javascript">import React from 'react';

const AddUser = (props) =&gt; {
    return (
      &lt;form onSubmit={(event) =&gt; props.addUser(event)}&gt;
          &lt;div className=&quot;form-group&quot;&gt;
            &lt;input
              type=&quot;text&quot; name=&quot;username&quot;
              className=&quot;form-control input-lg&quot;
              placeholder=&quot;Enter a username&quot;
              required
              value={props.username}
              onChange={props.handleChange}
            /&gt;
          &lt;/div&gt;
          &lt;div className=&quot;form-group&quot;&gt;
            &lt;input
              name=&quot;email&quot;
              className=&quot;form-control input-lg&quot;
              type=&quot;email&quot;
              placeholder=&quot;Enter an email address&quot;
              required
              value={props.email}
              onChange={props.handleChange}
            /&gt;
          &lt;/div&gt;
          &lt;input
            type=&quot;submit&quot;
            className=&quot;btn btn-primary btn-lg btn-block&quot;
            value=&quot;Submit&quot;
          /&gt;
        &lt;/form&gt;
    );
};

export default AddUser;
</code></pre>

<p>现在我能在去浏览器中测试下，已经正常工作了。我们可以在 addUser 方法中打印下 state 对象的值，方便我们来了解 state 对象：</p>

<pre><code class="language-javascript">addUser(event) {
  event.preventDefault();
  console.log('sanity check!');
  console.log(this.state);
};
</code></pre>

<p>然后我们可以到浏览器中输入用户名和邮件，点击提交按钮，我们可以在浏览器的 Console 终端中看到如下的信息了：
<img src="https://blog.qikqiak.com/img/posts/WX20180124-183550.png" alt="index" /></p>

<p>现在我们获得了输入的值，接下来我们就可以发送一个<code>AJAX</code>请求，将我们的输入添加到数据库中，然后更新 DOM 树&hellip;</p>

<h3 id="发送-ajax-请求">发送 AJAX 请求</h3>

<p>还记得我们前面的 users-service 服务中的<code>add_user</code>方法吗（<code>project/api/views.py</code>文件下）？我们添加一个新的用户需要发送 username 和 email 两项数据：</p>

<pre><code class="language-python">db.session.add(User(username=username, email=email))
</code></pre>

<p>现在我们在 React 代码中用<code>Axios</code>来发送添加用户的 POST 请求，修改<code>client/src/index.js</code>中 App 组件的<code>addUser</code>方法：</p>

<pre><code class="language-javascript">addUser(event) {
  event.preventDefault();
  console.log('sanity check!');
  const data = {
    username: this.state.username,
    email: this.state.email
  };
  axios.post(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`, data).then(res =&gt; {
    console.log(res);
  }).catch(err =&gt; {
    console.log(err);
  });
}
</code></pre>

<p>然后我们切换到浏览器中添加一条数据，只要输入的 email 地址是唯一的，测试应该都会通过的。如果我们连续点击提交两次，我们就可以看到在 Chrome 浏览器的终端中看到有一个<strong>400</strong>的错误码打印出来，因为我们第二次提交的 email 邮箱已经存在了，而我们前面的<code>users-service</code>服务在 email 存在的情况下会返回 400 状态码。
<img src="https://blog.qikqiak.com/img/posts/WX20180126-110951.png" alt="status400" /></p>

<h3 id="更新页面">更新页面</h3>

<p>最后当添加用户的表单提交成功后我们来更新用户列表，并且清空表单，同样更新<code>addUser</code>方法：</p>

<pre><code class="language-javascript">addUser(event) {
  event.preventDefault();
  console.log('sanity check!');
  const data = {
    username: this.state.username,
    email: this.state.email
  };
  axios.post(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`, data).then(res =&gt; {
    this.getUsers();
    this.setState({ username: '', email: '' });
  }).catch(err =&gt; {
    console.log(err);
  });
}
</code></pre>

<p>我们前面知道获取用户列表是通过<code>getUsers()</code>方法的，所以当我们添加用户的请求发送成功后，是不是重新请求下<code>getUsrs()</code>方法，是不是就能够把数据库中的所有用户获取到了啊？要清空表单该怎么做呢？当然是操作状态了，我们知道只需要更改 state 对象下的 username 和 email，对应的元素就会发送变化，我们不需要自己手动去更新 DOM 元素，只需要操作 state 就行，是不是很方便了啊？</p>

<p>然后我们切换到浏览器中测试一下，添加一个唯一的邮箱，是不是提交完成后下面的列表也更新了，表单也清空了。然后我们可以运行下我们的测试代码：</p>

<pre><code class="language-shell">$ npm run test
</code></pre>

<p>我们会发现测试是没有通过的，出现下面的错误提示，这是因为我们的 UI 界面已经发生了改变，和之前我们的快照是不一样的，所以这个时候我们只需要输入<code>u</code>更新下快照即可。</p>

<pre><code class="language-shell">Snapshot Summary
 › 1 snapshot test failed in 1 test suite. Inspect your code changes or press `u` to update them.
</code></pre>

<p>快照更新后，正常情况下测试代码都会通过的。如果有其他问题，请仔细查看下错误日志，仔细排查，然后提交代码到<code>github</code>。</p>

<h2 id="9-容器化-react-应用-a-id-docker-a">9. 容器化 React 应用<a id="docker"></a></h2>

<p>这节课让我们来容器化上面完成的 React 应用。不过在开始前，我们先重构下我们的项目吧。</p>

<h3 id="重构">重构</h3>

<p>在项目根目录下面新增一个叫<code>services</code>的文件夹，然后将<code>nginx</code>文件夹移动到 services 目录下面，将<code>client</code>文件夹移动到 services 目录下面，然后在 services 目录下面新建一个 <code>users</code>文件夹，将根目录下面的<code>project</code>文件夹、Dockerfile、manage.py、requirements.txt 文件都移动到<code>services/users</code>目录下面，操作完成后，你看到的我们的代码结构应该是这样的：</p>

<pre><code class="language-shell">$ flask-microservices-users [master] ⚡ ls -la
total 88
drwxr-xr-x  12 ych  staff    384  1 26 12:12 .
drwxr-xr-x   4 ych  staff    128 12 28 16:08 ..
drwxr-xr-x  14 ych  staff    448  1 26 12:27 .git
-rw-r--r--   1 ych  staff     23  1  5 17:56 .gitignore
-rw-r--r--   1 ych  staff      5 12 28 15:19 .python-version
-rw-r--r--   1 ych  staff    645  1  5 18:13 .travis.yml
-rw-r--r--   1 ych  staff  11357 12 29 10:44 LICENSE
-rw-r--r--   1 ych  staff    206  1  5 18:23 README.md
-rw-r--r--   1 ych  staff    922  1 26 12:11 docker-compose-prod.yml
-rw-r--r--   1 ych  staff    755  1 26 12:15 docker-compose.yml
drwxr-xr-x   6 ych  staff    192  1 26 12:09 services
$ flask-microservices-users [master] ⚡ ls -la services
total 16
drwxr-xr-x   6 ych  staff   192  1 26 12:09 .
drwxr-xr-x  12 ych  staff   384  1 26 12:12 ..
drwxr-xr-x  10 ych  staff   320  1 16 17:37 client
drwxr-xr-x   4 ych  staff   128 12 30 15:15 nginx
drwxr-xr-x   7 ych  staff   224  1 26 12:14 users
$ flask-microservices-users [master] ⚡ ls -la services/users
total 40
drwxr-xr-x  7 ych  staff   224  1 26 12:14 .
drwxr-xr-x  6 ych  staff   192  1 26 12:09 ..
-rw-r--r--  1 ych  staff   329 12 29 10:47 Dockerfile
-rw-r--r--  1 ych  staff  1366  1  5 17:46 manage.py
drwxr-xr-x  9 ych  staff   288 12 31 10:56 project
-rw-r--r--  1 ych  staff   159  1 13 14:28 requirements.txt
</code></pre>

<p>然后我们来更新<code>docker-compose.yml</code>文件：</p>

<pre><code class="language-yaml">version: '3.3'

services:
  users-db:
    container_name: users-db
    build:
      context: ./services/users/project/db
      dockerfile: Dockerfile
    ports:
      - 3307:3306
    environment:
      - MYSQL_ROOT_PASSWORD=root321

  users-service:
    container_name: users-service
    build:
      context: ./services/users
      dockerfile: Dockerfile-dev
    volumes:
      - './services/users:/usr/src/app'
    ports:
      - 5001:5000 # 暴露端口 - 主机:容器
    environment:
      - APP_SETTINGS=project.config.DevelopmentConfig
      - DATABASE_URL=mysql+pymysql://root:root321@users-db:3306/users_dev
      - DATABASE_TEST_URL=mysql+pymysql://root:root321@users-db:3306/users_test
    depends_on:
      - users-db
    links:
      - users-db
</code></pre>

<p>同样的，更新<code>docker-compose-prod.yml</code>文件：</p>

<pre><code class="language-yaml">version: '3.3'

services:
  users-db:
    container_name: users-db
    build:
      context: ./services/users/project/db
      dockerfile: Dockerfile
    ports:
      - 3307:3306
    environment:
      - MYSQL_ROOT_PASSWORD=root321

  users-service:
    container_name: users-service
    build:
      context: ./services/users
      dockerfile: Dockerfile-prod
    expose:
      - '5000' # 只暴露容器端口
    environment:
      - APP_SETTINGS=project.config.ProductionConfig
      - DATABASE_URL=mysql+pymysql://root:root321@users-db:3306/users_prod
      - DATABASE_TEST_URL=mysql+pymysql://root:root321@users-db:3306/users_test
    command: gunicorn -b 0.0.0.0:5000 manage:app
    depends_on:
      - users-db
    links:
      - users-db

  nginx:
    container_name: nginx
    build: ./services/nginx
    restart: always
    ports:
      - 8080:80
    depends_on:
      - users-service
    links:
      - users-service
</code></pre>

<p>注意上面的<code>docker-compose</code>文件我们更新到<code>3.3</code>版本，注意看和之前的<code>2.1</code>版本是有部分区别的。然后重新构建下我们的容器：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml up -d --build
</code></pre>

<p>确保上面的构建过程是正确的，我们可以通过查看<code>users-service</code>服务的日志来确认：</p>

<pre><code class="language-shell">$ docker-compose logs -f users-service
Attaching to users-service
users-service    |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
users-service    |  * Restarting with stat
users-service    |  * Debugger is active!
users-service    |  * Debugger PIN: 170-528-240
users-service    | 172.18.0.1 - - [26/Jan/2018 04:22:08] &quot;GET /ping HTTP/1.1&quot; 200 -
</code></pre>

<p>然后我们可以跑一遍我们的测试用例，确保的业务逻辑也是符合我们的预期的：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml run users-service python manage.py test
</code></pre>

<p>如果一切正常，上面的测试也会正常通过的。</p>

<h3 id="本地开发环境">本地开发环境</h3>

<p>在<code>services/client</code>目录下面添加<code>Dockerfile-dev</code>文件：</p>

<pre><code class="language-yaml">FROM node:9.4
MAINTAINER &quot;qikqiak@gmail.com&quot;

# 设置工作目录
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# 添加node_modules/.bin目录到 $PATH 环境下
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# 安装并缓存依赖包(用silent选项可以减少一些不必要的错误)
ADD package.json /usr/src/app/package.json
RUN npm install --silent
RUN npm install react-scripts@1.0.17 -g --silent

# 启动应用
CMD [&quot;npm&quot;, &quot;start&quot;]
</code></pre>

<p>然后我们在<code>docker-compose.yml</code>中新增 React 应用的服务：</p>

<pre><code class="language-yaml">client:
  container_name: client
  build:
    context: ./services/client
    dockerfile: Dockerfile-dev
  volumes:
    - './services/client:/usr/src/app'
  ports:
    - '3007:3000'
  environment:
    - NODE_ENV=development
    - REACT_APP_USERS_SERVICE_URL=${REACT_APP_USERS_SERVICE_URL}
  depends_on:
    - users-service
  links:
    - users-service
</code></pre>

<p>在确保<code>users-service</code>服务正常启动后，在终端中导入环境变量：</p>

<pre><code class="language-shell">$ export REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001
</code></pre>

<p>然后重新构建镜像启动新的容器：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml up --build -d client
</code></pre>

<p>新容器启动完成后，运行 React 客户端测试代码：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml run client npm test
</code></pre>

<p>测试通过后，我们可以在本地浏览器中打开<code>http://127.0.0.1:3007</code>访问 React 应用了，当然如果测试没通过的话，我们得根据错误信息去修复我们的测试代码，直到测试通过为止。</p>

<p>更新<code>services/nginx/flask.conf</code>配置：</p>

<pre><code class="language-nginx">server {
  listen 80;

  location / {
    proxy_pass http://client:3000;
    proxy_redirect    default;
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
  }

  location /users {
    proxy_pass        http://users-service:5000;
    proxy_redirect    default;
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
  }
}
</code></pre>

<p>上面的<code>nginx</code>配置文件做了什么工作？</p>

<ul>
<li>首先在<code>location</code>块中定义了反向代理规则。</li>
<li>当请求的 URI 与<code>location</code>区域中的 URI 匹配的时候，<code>Nginx</code>将请求转发给了后面的 React 服务（client）或者 Flask 服务（users-service）。</li>
</ul>

<p>同样的，我们将<code>Nginx</code>服务也定义到<code>docker-compose.yml</code>文件中：</p>

<pre><code class="language-yaml">nginx:
  container_name: nginx
  build: ./services/nginx
  restart: always
  ports:
    - 8080:80
  depends_on:
    - users-service
    - client
  links:
    - users-service
</code></pre>

<p>然后更新容器：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml up -d --build
</code></pre>

<blockquote>
<p>注意我们这里用的主机端口为8080，因为我们<strong>本机的80端口已经被占用</strong>，在实际线上部署的时候应该用80端口，然后再根据域名进行匹配进行测试。</p>
</blockquote>

<p>容器启动完成后，打开浏览器测试下我们的服务，确保下面的两个请求都是正常的：</p>

<ul>
<li><a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a></li>
<li><a href="http://127.0.0.1:8080/users">http://127.0.0.1:8080/users</a></li>
</ul>

<p>我们可以通过下面的命令查看运行中的容器日志：</p>

<pre><code class="language-shell">$ docker-compose -f docker-compose.yml logs -f
</code></pre>

<p>然后我们可以改变下 App 组件的 state 对象：</p>

<pre><code class="language-javascript">this.state = {
  users: [],
  username: 'test001',
  email: ''
};
</code></pre>

<p>当我们保存的时候可以看到日志中输出了编译成功的信息，而且这个时候浏览器也自动刷新了，并且将 test001 填充在了用户名的输入框中。
<img src="https://blog.qikqiak.com/img/posts/WX20180224-170838.png" alt="change state" />
当然测试过后，要记得将上面的 state 对象的值更改回去哦~</p>

<h3 id="生产环境">生产环境</h3>

<p>在将 React 应用上到生产环境之前我们需要将应用进行打包构建用于生成一系列的静态文件，我们在<code>services/client</code>目录下面的<code>package.json</code>文件中可以看到有一个<code>build</code>的脚本，该脚本就是用于我们对 React 应用进行打包的，在该目录下面执行命令：</p>

<pre><code class="language-shell">$ npm run build

&gt; client@0.1.0 build /Users/ych/devs/workspace/yidianzhishi/tdd100/flask-microservices-users/services/client
&gt; react-scripts build

Creating an optimized production build...
Compiled successfully.
</code></pre>

<p>构建完成后我们可以发现在该目录下面多了一个<strong>build</strong>的目录，该目录下面打包过后的静态资源文件。我们需要一个静态服务器来访问这些静态资源，执行下面的命令：</p>

<pre><code class="language-shell">$ npm install -g serve
$ serve -s build
   ┌──────────────────────────────────────────────────┐
   │                                                  │
   │   Serving!                                       │
   │                                                  │
   │   - Local:            http://localhost:5000      │
   │   - On Your Network:  http://192.168.31.9:5000   │
   │                                                  │
   │   Copied local address to clipboard!             │
   │                                                  │
   └──────────────────────────────────────────────────┘
</code></pre>

<p>我们可以根据上面的提示访问<code>http://localhost:5000</code>，然后我们可以在进行一次相关的测试，确保没有任何错误。</p>

<p>然后在<code>client</code>的根目录下面新增一个用于生产环境的 Dockerfile 文件：(<strong>Dockerfile-prod</strong>)</p>

<pre><code class="language-Dockerfile"># build environment
FROM node:9.4 as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
ARG REACT_APP_USERS_SERVICE_URL
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
ENV REACT_APP_USERS_SERVICE_URL $REACT_APP_USERS_SERVICE_URL
COPY package.json /usr/src/app/package.json
RUN npm install --silent
RUN npm install react-scripts@1.1.0 -g --silent
COPY . /usr/src/app
RUN npm run build

# production environment
FROM nginx:1.13.5-alpine
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
EXPOSE 80
CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]
</code></pre>

<p>我们可以看到上面的<code>Dockerfile</code>文件和之前的不太一样，出现了两个基础镜像，这是<code>Docker</code>提供的多阶段构建功能，详细的可以参考我之前的一篇博文：<a href="https://blog.qikqiak.com/post/multi-stage-build-for-docker/">Docker 的多阶段构建</a>。</p>

<p>当构建镜像的时候，参数通过<code>ARG</code>指令传递给<code>Dockerfile</code>，然后就可以将其用做环境变量。<code>npm run build</code>命令将生成静态文件，然后通过<strong>Nginx</strong>暴露80端口提供服务。</p>

<p>接下来我们不使用<code>Docker Compose</code>来进行一些简单的测试。首先，在<code>services/client</code>目录下面构建镜像，记得使用<code>--build-arg</code>标记来传递合适的参数：</p>

<pre><code class="language-shell">$ docker build -f Dockerfile-prod -t &quot;test&quot; ./ --build-arg NODE_ENV=development --build-arg REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001
</code></pre>

<blockquote>
<p>记得将上面的<strong>REACT_APP_USERS_SERVICE_URL</strong>替换成你自己的实际的 IP。</p>
</blockquote>

<p>上面的构建命令是用<code>services/client</code>下面的<code>Dockerfile-prod</code>文件来构建一个叫 test 的镜像。</p>

<p>镜像构建完成后我们使用上面的 test 镜像来启动一个名为 test01 的容器，将主机的9000端口映射到容器的80端口：</p>

<pre><code class="language-shell">$ docker run --name test01 -d -p 9000:80 test
</code></pre>

<p>容器启动成功后，我们可以在浏览器中打开链接<a href="http://localhost:9000/">http://localhost:9000/</a>来进行相关的测试。
然后我们可以停止容器：</p>

<pre><code class="language-shell">$ docker stop test01
$ docker rm test01
</code></pre>

<p>最好，删除上面的镜像：</p>

<pre><code class="language-shell">$ docker rmi test
</code></pre>

<p>上面我们对<code>Dockerfile-prod</code>进行了相关的设置和测试，现在我们将该服务添加到<code>docker-compose-prod.yml</code>：</p>

<pre><code class="language-yaml">client:
  container_name: client
  build:
    context: ./services/client
    dockerfile: Dockerfile-prod
    args:
      - NODE_ENV=production
      - REACT_APP_USERS_SERVICE_URL=${REACT_APP_USERS_SERVICE_URL}
  ports:
    - '3007:80'
  depends_on:
    - users
  links:
    - users
</code></pre>

<p>注意这里我们使用的是<code>args</code>，而不是环境变量了。另外，我们的这个 client 服务需要在 nginx 服务启动之前启动，所以我们相应的更新<strong>docker-compose-prod.yml</strong>：</p>

<pre><code class="language-yaml">nginx:
  container_name: nginx
  build: ./services/nginx
  restart: always
  ports:
    - 80:80
  depends_on:
    - users
    - client
</code></pre>

<blockquote>
<p>TODO&hellip;</p>
</blockquote>

<p>扫描下面的二维码(或微信搜索<code>iEverything</code>)添加我微信好友(注明<code>python</code>)，然后可以加入到我们的<code>python</code>讨论群里面共同学习
<img src="https://blog.qikqiak.com/img/posts/wexin-qrcode.jpeg" alt="qrcode" /></p>
      </article>

      
        

<h3>相关文章</h3>
<ul style="margin-bottom: 25px;">
    
    <li><a href="https://blog.qikqiak.com/post/tdd-develop-python-microservice-app/">TDD开发容器化的Python微服务应用(一)</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/python-convert-pdf-images/">利用Python 优雅的将PDF 转换成图片</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/django-staticroot-staticfilesdirs-function/">Django 中STATIC_ROOT 与STATICFILES_DIRS的区别</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/django-decorator-usage/">Python装饰器简介</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/python-process-csv-file/">用python处理csv文件</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/python-virtualenv-all-know/">Python 虚拟环境全知道</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/install-docker-registry-harbor-in-kubernetes/">在kubernetes 集群上搭建docker 私有仓库Harbor</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/manual-install-high-available-kubernetes-cluster/">手动搭建高可用的kubernetes 集群</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/upgrading-django-20-10-tips/">更新django2.0的10条注意事项</a></li>
    
    <li><a href="https://blog.qikqiak.com/post/webpack3-get-started/">webpack3入门到放肆视频教程</a></li>
    
</ul>

      

      
      <ul class="pager blog-pager">
        
          <li class="previous">
            <a href="https://blog.qikqiak.com/post/python-virtualenv-all-know/" data-toggle="tooltip" data-placement="top" title="Python 虚拟环境全知道">&larr; Previous Post</a>
          </li>
        
        
          <li class="next">
            <a href="https://blog.qikqiak.com/post/add-authorization-for-kubernetes-dashboard/" data-toggle="tooltip" data-placement="top" title="为kubernetes dashboard访问用户添加权限控制">Next Post &rarr;</a>
          </li>
        
      </ul>
      

      

      
      <div id="git-comments"></div>
      <link rel="stylesheet" href="https://imsun.github.io/gitment/style/default.css">
      <script src="https://imsun.github.io/gitment/dist/gitment.browser.js"></script>
      <script>
      var gitment = new Gitment({
        id: 'tdd-develop-python-microservice-app-part2',
        owner: 'cnych',
        repo: 'blog',
        oauth: {
          client_id: 'bdb76dbb2e9d0786e350',
          client_secret: 'b454b2a08013fd0e32013be7a63fa8fcb262b6c4',
        }
      })
      gitment.render('git-comments')
      </script>
      

    </div>
  </div>
</div>

    <footer>
  <div class="container">
    <div class="row">
      <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        <ul class="list-inline text-center footer-links">
          
              <li>
                <a href="mailto:icnych@gmail.com" title="Email me">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-envelope fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
              <li>
                <a href="https://github.com/cnych" title="GitHub">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-github fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
              <li>
                <a href="https://weibo.com/cnych" title="微博">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-weibo fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
              <li>
                <a href="https://instagram.com/cnych" title="Instagram">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-instagram fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
          
          <li>
            <a href="https://blog.qikqiak.com/index.xml" title="RSS">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-rss fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
          
        </ul>
        <p class="credits copyright text-muted">
          

          &nbsp;&bull;&nbsp;
          2018

          
            &nbsp;&bull;&nbsp;
            <a href="https://blog.qikqiak.com/">阳明的博客</a>
          
        </p>
        
        <p class="credits theme-by text-muted">
          <a href="http://gohugo.io">Hugo v0.34</a> powered &nbsp;&bull;&nbsp; Theme by <a href="http://deanattali.com/beautiful-jekyll/">Beautiful Jekyll</a> adapted to <a href="https://github.com/halogenica/beautifulhugo">Beautiful Hugo</a>
          
        </p>
      </div>
    </div>
  </div>
</footer>

<script src="https://cdn.bootcss.com/KaTeX/0.7.1/katex.min.js"></script>
<script src="https://cdn.bootcss.com/KaTeX/0.7.1/contrib/auto-render.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://blog.qikqiak.com/js/main.min.js"></script>
<script src="https://blog.qikqiak.com/js/highlight.min.js"></script>
<script> hljs.initHighlightingOnLoad(); </script>
<script> renderMathInElement(document.body); </script>
<script src="https://blog.qikqiak.com/js/prism.js?t=123"></script>
<script src="https://cdn.bootcss.com/photoswipe/4.1.1/photoswipe.min.js"></script>
<script src="https://cdn.bootcss.com/photoswipe/4.1.1/photoswipe-ui-default.min.js"></script>
<script src="https://blog.qikqiak.com/js/load-photoswipe.min.js"></script>



<script async src="https://www.googletagmanager.com/gtag/js?id=UA-69668147-3"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'UA-69668147-3');
</script>

  </body>
</html>

